/**
 * Copyright (C) 2012 Eduard Sedov
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.es.function;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.es.function.impl.FunctionOverload;
import org.es.function.impl.FunctionReflection;
import org.es.function.impl.NonStaticMethodFilter;
import org.es.function.impl.NonStaticPredicateMethodFilter;
import org.es.function.impl.ObjectMethodFilter;
import org.es.function.impl.StaticMethodFilter;
import org.es.function.impl.StaticPredicateMethodFilter;
import org.es.registry.Node;

/**
 * A function registry is a entry point place for searching of functions via
 * their names.
 *
 * @author ESEDO
 */
public class FunctionRegistry {

    private static FunctionRegistry instance = new FunctionRegistry();
    private Node functions;
    private MethodFilter defaultMethodFilter = ObjectMethodFilter.get();

    public FunctionRegistry() {
        this(new Node());
    }

    public FunctionRegistry(Node functions) {
        this.functions = functions;
    }

    public static FunctionRegistry get() {
        return instance;
    }

    /**
     * Puts the given function to this registry and associates it with the given
     * name.
     *
     * @param name the name
     * @param function the function
     * @return the function registry self
     */
    public FunctionRegistry add(String name, Function function) {
        if (name == null) {
            throw new IllegalArgumentException("Parameter 'name' may not be null.");
        }
        if (function == null) {
            throw new IllegalArgumentException("Parameter 'function' may not be null.");
        }
        if (functions.isSet(name)) {
            throw new IllegalArgumentException("A function with name '" + name + "' is already exist.");
        }

        functions.set(name, function);

        return this;
    }

    /**
     * Returns an with the given name associated function if such exist.
     *
     * @param name the name of the function
     * @return the function or {@code null} if no such function exist
     */
    public <T> Function<T> get(String name) {
        return functions.get(name);
    }

    /**
     * Remove the previously added function.
     *
     * @param name the name of function
     * @return the removed function or {@code null} if no such function exist
     */
    public <T> Function<T> remove(String name) {
        return functions.remove(name);
    }

    /**
     * Returns a set with names of all registered functions.
     *
     * @return a set with names
     */
    public Set<String> getFunctionNames() {
        return Collections.unmodifiableSet(functions.list());
    }

    /**
     * Call a function with the given name.
     *
     * @param name the name of function to call
     * @param params the parameter value to pass to the function
     * @return the return value of function call
     */
    public Object invoke(String name, Object... params) {
        Function function = functions.get(name);

        if (function == null) {
            throw new IllegalArgumentException("No such function '" + name + "' found.");
        }

        return function.invoke(params);
    }

    /**
     * Creates a proxy object for given interface class and maps all methods
     * annotated with {@link FunctionAnnotation} to the appropriate
     * function in this registry.
     *
     * <br><br> Let's say there are two functions 'min' and 'max' registered by
     * the function registry and we have following interface:
     * <pre>
     * <code>public inteface MinMax {
     *
     *     &#064;FunctionAnnotation("min")
     *     public int minOfInteger(int p1, int p2);
     *
     *     &#064;FunctionAnnotation("max")
     *     public int maxOfInteger(int p1, int p2);
     * }
     * </code></pre> than this method returns for such interface a proxy which
     * methods are mapped to 'min' and 'max' functions from the registry. Now we
     * can use it in our program:
     * <pre><code>
     * ...
     * MinMax minMax = FunctionRegistry.create(MinMax.class, FunctionRegistry.get());
     * int min = minMax.minOfInteger(1, 2);  // The 'min' function will be called and the 'min' variable gets value 1.
     * int max = minMax.maxOfInteger(1, 2);  // The 'max' function will be called and the 'max' variable gets value 2.
     * ...
     * </code></pre>
     *
     * @param <T> generic type of return object
     * @param interfaceClass the class of interface
     * @return the created proxy object that implements the given interface
     */
    public <T> T create(Class<T> interfaceClass) {
        if (interfaceClass.isInterface() == false) {
            throw new IllegalArgumentException("Only interface classes supported.");
        }

        // checks that the appropriate functions exist for all annotated methods
        for (Method method : interfaceClass.getMethods()) {
            FunctionAnnotation annot = method.getAnnotation(FunctionAnnotation.class);
            if (annot != null) {
                String functionName = annot.value();
                if (functionName == null || functionName.isEmpty()) {
                    functionName = method.getName();
                }
                if (get(functionName) == null) {
                    throw new IllegalArgumentException("No such function '" + functionName + "' found.");
                }
            }
        }

        // create a proxy for given interface
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{interfaceClass}, new FunctionInvocationHandler(this));
    }

    /**
     * An implementation of {@link InvocationHandler} used to create proxies to
     * functions registered in function registry.
     */
    private static class FunctionInvocationHandler implements InvocationHandler {

        private FunctionRegistry functionRegistry;
        private Map<Method, FunctionAnnotation> annotationMap = new HashMap();

        public FunctionInvocationHandler(FunctionRegistry functionRegistry) {
            this.functionRegistry = functionRegistry;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            FunctionAnnotation annot = annotationMap.get(method);
            if (annot == null) {
                annot = method.getAnnotation(FunctionAnnotation.class);
                annotationMap.put(method, annot);
            }
            if (annot == null) {
                throw new UnsupportedOperationException("Method " + method + " has no '" + FunctionAnnotation.class.getSimpleName() + "' annotation.");
            }

            String functionName = annot.value();

            if (functionName == null || functionName.isEmpty()) {
                functionName = method.getName();
            }

            return functionRegistry.invoke(functionName, args);
        }
    }

    /**
     * Utility method to register all static methods of the given class.
     *
     * @param ownerClass class
     */
    public void registerStaticFunctions(Class ownerClass) {
        registerStaticFunctions(ownerClass, null, null);
    }

    /**
     * Utility method to register static methods of the given class.
     *
     * @param ownerClass class
     * @param methodFilter filter
     */
    public void registerStaticFunctions(Class ownerClass, MethodFilter methodFilter) {
        registerStaticFunctions(ownerClass, methodFilter, null);
    }

    /**
     * Utility method to register all static methods of the given class.
     *
     * @param ownerClass class
     * @param namePrefix the name prefix that will be set for each function
     */
    public void registerStaticFunctions(Class ownerClass, String namePrefix) {
        registerStaticFunctions(ownerClass, null, namePrefix);
    }

    /**
     * Utility method to register static methods of the given class.
     *
     * @param ownerClass class
     * @param methodFilter filter
     * @param namePrefix the name prefix that will be set for each function
     */
    public void registerStaticFunctions(Class ownerClass, MethodFilter methodFilter, String namePrefix) {
        Method[] methods = ownerClass.getMethods();
        registerFunctions(null, methods, methodFilter, namePrefix);
    }

    /**
     * Utility method to register the given static methods.
     *
     * @param ownerClass class
     * @param namePrefix the name prefix that will be set for each function
     */
    public void registerStaticFunctions(Method[] methods, String namePrefix) {
        registerFunctions(null, methods, StaticPredicateMethodFilter.get(), namePrefix);
    }

    /**
     * Utility method to register the all non static methods of the given
     * object.
     *
     * @param instance object
     */
    public void registerFunctions(Object instance) {
        registerFunctions(instance, (MethodFilter) null, null);
    }

    public void registerFunctions(Class ownerClass, MethodFilter methodFilter) {
        registerFunctions(ownerClass, methodFilter, null);
    }

    public void registerFunctions(Object instance, String namePrefix) {
        registerFunctions(instance, (MethodFilter) null, namePrefix);
    }

    public void registerFunctions(Object instance, MethodFilter methodFilter, String namePrefix) {
        Method[] methods = instance.getClass().getMethods();
        registerFunctions(instance, methods, methodFilter, namePrefix);
    }

    public void registerFunctions(Object instance, Method[] methods, String namePrefix) {
        registerFunctions(null, methods, NonStaticPredicateMethodFilter.get(), namePrefix);
    }

    /**
     * Method that registers the given methods as function objects.
     *
     * @param targetInstance the target instance for non static methods.
     * @param methods
     * @param methodFilter
     * @param namePrefix
     */
    protected void registerFunctions(Object targetInstance, Method[] methods, MethodFilter methodFilter, String namePrefix) {

        Map<String, Object> map = new HashMap();

        for (Method method : methods) {

            if (!defaultMethodFilter.accept(method)) {
                continue;
            }

            if (methodFilter != null && !methodFilter.accept(method)) {
                continue;
            }

            if (targetInstance == null && NonStaticMethodFilter.get().accept(method)) {
                continue;
            }

            if (!method.isAccessible()) {
                method.setAccessible(true);
            }

            String functionName = namePrefix == null ? method.getName() : (namePrefix + method.getName());

            Object o = map.get(functionName);

            if (o == null) {
                map.put(functionName, method);
            } else if (o instanceof Method) {
                List list = new ArrayList();
                list.add(o);
                list.add(method);
                map.put(functionName, list);
            } else if (o instanceof List) {
                ((List) o).add(method);
            }
        }

        for (Map.Entry<String, Object> entry : map.entrySet()) {

            Function existingFunction = get(entry.getKey());

            if (entry.getValue() instanceof List) {

                FunctionOverload function;

                if (existingFunction instanceof FunctionOverload) {
                    function = (FunctionOverload) existingFunction;
                    remove(entry.getKey());
                } else if (existingFunction instanceof FunctionReflection) {
                    function = new FunctionOverload();
                    function.add(existingFunction, ((FunctionReflection) existingFunction).getMethod().getParameterTypes());
                    remove(entry.getKey());
                } else {
                    function = new FunctionOverload();
                }

                for (Method method : (List<Method>) entry.getValue()) {
                    if (StaticMethodFilter.get().accept(method)) {
                        function.add(new FunctionReflection(method, null), method.getParameterTypes());
                    } else {
                        function.add(new FunctionReflection(method, targetInstance), method.getParameterTypes());
                    }
                }

                add(entry.getKey(), function);

            } else if (entry.getValue() instanceof Method) {

                FunctionReflection function = new FunctionReflection((Method) entry.getValue(), targetInstance);

                if (existingFunction instanceof FunctionOverload) {
                    ((FunctionOverload) existingFunction).add(function, function.getMethod().getParameterTypes());
                } else if (existingFunction instanceof FunctionReflection) {
                    FunctionOverload tmp = new FunctionOverload();
                    tmp.add(existingFunction, ((FunctionReflection) existingFunction).getMethod().getParameterTypes());
                    tmp.add(function, function.getMethod().getParameterTypes());
                    remove(entry.getKey());
                    add(entry.getKey(), tmp);
                } else {
                    add(entry.getKey(), function);
                }
            }
        }
    }
}
